﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TopDownOperatorPrecedence
{

    public class PObject
    {
        private PEnvironment members;
        public PEnvironment Members
        {
            get
            {
                return this.members;
            }
        }

        public static readonly PObject Null = new PObject();

        public PObject()
        {
            this.members = new PEnvironment();
        }

        public PObject InvokeBinaryOperation(string name, PObject value)
        {
            switch (name)
            {
                case "+":
                    return this.Add(value);
                case "-":
                    return this.Sub(value);
                case "*":
                    return this.Mul(value);
                case "/":
                    return this.Div(value);
                case "%":
                    return this.Mod(value);
                case "+=":
                    return this.AddAssign(value);
                case "-=":
                    return this.SubAssign(value);
                case "*=":
                    return this.MulAssign(value);
                case "/=":
                    return this.DivAssign(value);
                case "%=":
                    return this.ModAssign(value);
                case "[":
                    return this.IndexGet(value);
                case "<":
                    return this.LessThan(value);
                case ">":
                    return this.GreaterThan(value);
                case "<=":
                    return this.LessThanOrEqualsTo(value);
                case ">=":
                    return this.GreaterThanOrEqualsTo(value);
                case "==":
                    return this.EqualsTo(value);
                case "!=":
                    return this.NotEqualsTo(value);
                default:
                    throw new Exception("Unrecognized operation " + name);
            }
        }

        public virtual PObject Clone()
        {
            var result = new PObject();
            result.members = new PEnvironment(this.members);
            return result;
        }

        public override string ToString()
        {
            return this.members.ToString();
        }

        public virtual PObject Add(PObject value)
        {
            throw new NotImplementedException();
        }

        public virtual PObject Sub(PObject value)
        {
            throw new NotImplementedException();
        }

        public virtual PObject Mul(PObject value)
        {
            throw new NotImplementedException();
        }

        public virtual PObject Div(PObject value)
        {
            throw new NotImplementedException();
        }

        public virtual PObject Mod(PObject value)
        {
            throw new NotImplementedException();
        }

        public virtual PObject AddAssign(PObject value)
        {
            throw new NotImplementedException();
        }

        public virtual PObject SubAssign(PObject value)
        {
            throw new NotImplementedException();
        }

        public virtual PObject MulAssign(PObject value)
        {
            throw new NotImplementedException();
        }

        public virtual PObject DivAssign(PObject value)
        {
            throw new NotImplementedException();
        }

        public virtual PObject ModAssign(PObject value)
        {
            throw new NotImplementedException();
        }

        public virtual PObject DotGet(PObject value)
        {
            if (value is PString)
            {
                string name = (value as PString).Value;
                return this.members.Lookup(name);
            }
            else
            {
                throw new Exception("invalid dot expression");
            }
        }

        public virtual PObject DotSet(PObject nameObject, PObject value)
        {
            if (nameObject is PString)
            {
                string name = (nameObject as PString).Value;
                this.members.DefineAtCurrentScope(name, value);
                return value;
            }
            else
            {
                throw new Exception("invalid dot expression");
            }
        }

        public virtual PObject IndexGet(PObject index)
        {
            throw new NotImplementedException();
        }

        public virtual PObject IndexSet(PObject index, PObject newValue)
        {
            throw new NotImplementedException();
        }

        public virtual PObject Invoke(List<PObject> arguments, PEnvironment environment)
        {
            throw new NotImplementedException();
        }

        public virtual PObject DotMember(string name)
        {
            throw new NotImplementedException();
        }

        public virtual PBool EqualsTo(PObject value)
        {
            throw new NotImplementedException();
        }

        public virtual PBool NotEqualsTo(PObject value)
        {
            throw new NotImplementedException();
        }

        public virtual PBool GreaterThan(PObject value)
        {
            throw new NotImplementedException();
        }

        public virtual PBool GreaterThanOrEqualsTo(PObject value)
        {
            throw new NotImplementedException();
        }

        public virtual PBool LessThan(PObject value)
        {
            throw new NotImplementedException();
        }

        public virtual PBool LessThanOrEqualsTo(PObject value)
        {
            throw new NotImplementedException();
        }

        public virtual string ToRep()
        {
            return this.ToString();
        }

    }

    public class PBool : PObject
    {
        public static readonly PBool True = new PBool();
        public static readonly PBool False = new PBool();
        public override string ToString()
        {
            if (this == True)
            {
                return "True";
            }
            return "False";
        }

        public override PObject Clone()
        {
            if (this == PBool.True)
            {
                return PBool.True;
            }
            return PBool.False;
        }
    }

    public class PString : PObject
    {
        public string Value { get; private set; }
        public PString(string value)
        {
            this.Value = value;
        }

        public override string ToString()
        {
            return "'" + this.Value + "'";
        }

        public override string ToRep()
        {
            return this.Value;
        }

        public override PObject Add(PObject value)
        {
            return new PString(this.Value + value.ToRep());
        }

        public override PObject IndexGet(PObject index)
        {
            if (index is PNumber)
            {
                int actualIndex = (index as PNumber).Value;
                if (actualIndex < 0)
                {
                    actualIndex += this.Value.Length;
                }
                if (actualIndex < 0 || actualIndex > this.Value.Length)
                {
                    throw new Exception("index out of range");
                }

                return new PString(this.Value[actualIndex].ToString());
            }
            throw new Exception("index must be a number");
        }

        public override PObject IndexSet(PObject index, PObject newValue)
        {
            if (index is PNumber)
            {
                throw new Exception("string is immutable");
            }
            throw new Exception("index must be a number");
        }

        public override PObject Clone()
        {
            return new PString(this.Value);
        }
    }

    public class PNumber : PObject
    {
        public int Value { get; private set; }
        public PNumber(int value)
        {
            this.Value = value;
        }

        public override PObject Add(PObject value)
        {
            if (value is PNumber)
            {
                int newValue = this.Value + (value as PNumber).Value;
                return new PNumber(newValue);
            }
            throw new Exception("Could not add value " + value + " to a number");
        }

        public override PObject Sub(PObject value)
        {
            if (value is PNumber)
            {
                int newValue = this.Value - (value as PNumber).Value;
                return new PNumber(newValue);
            }
            throw new Exception("Could not sub value " + value + " to a number");
        }

        public override PObject Mul(PObject value)
        {
            if (value is PNumber)
            {
                int newValue = this.Value * (value as PNumber).Value;
                return new PNumber(newValue);
            }
            throw new Exception("Could not add value " + value + " to a number");
        }

        public override PObject Div(PObject value)
        {
            if (value is PNumber)
            {
                int newValue = this.Value / (value as PNumber).Value;
                return new PNumber(newValue);
            }
            throw new Exception("Could not div value " + value + " to a number");
        }

        public override PObject Mod(PObject value)
        {
            if (value is PNumber)
            {
                int newValue = this.Value % (value as PNumber).Value;
                return new PNumber(newValue);
            }
            throw new Exception("Could not mod value " + value + " to a number");
        }

        public override PObject AddAssign(PObject value)
        {
            if (value is PNumber)
            {
                this.Value += (value as PNumber).Value;
                return this;
            }
            throw new Exception("Could not add value " + value + " to a number");
        }

        public override PObject SubAssign(PObject value)
        {
            if (value is PNumber)
            {
                this.Value -= (value as PNumber).Value;
                return this;
            }
            throw new Exception("Could not sub value " + value + " to a number");
        }

        public override PObject MulAssign(PObject value)
        {
            if (value is PNumber)
            {
                this.Value *= (value as PNumber).Value;
                return this;
            }
            throw new Exception("Could not add value " + value + " to a number");
        }

        public override PObject DivAssign(PObject value)
        {
            if (value is PNumber)
            {
                this.Value /= (value as PNumber).Value;
                return this;
            }
            throw new Exception("Could not div value " + value + " to a number");
        }

        public override PObject ModAssign(PObject value)
        {
            if (value is PNumber)
            {
                this.Value %= (value as PNumber).Value;
                return this;
            }
            throw new Exception("Could not mod value " + value + " to a number");
        }

        public override PBool LessThan(PObject value)
        {
            if (value is PNumber)
            {
                return this.Value < (value as PNumber).Value ? PBool.True : PBool.False;
            }
            throw new Exception("Could not add value " + value + " to a number");
        }

        public override PBool LessThanOrEqualsTo(PObject value)
        {
            if (value is PNumber)
            {
                return this.Value <= (value as PNumber).Value ? PBool.True : PBool.False;
            }
            throw new Exception("Could not sub value " + value + " to a number");
        }

        public override PBool GreaterThan(PObject value)
        {
            if (value is PNumber)
            {
                return this.Value > (value as PNumber).Value ? PBool.True : PBool.False;
            }
            throw new Exception("Could not add value " + value + " to a number");
        }

        public override PBool GreaterThanOrEqualsTo(PObject value)
        {
            if (value is PNumber)
            {
                return this.Value >= (value as PNumber).Value ? PBool.True : PBool.False;
            }
            throw new Exception("Could not div value " + value + " to a number");
        }

        public override PBool EqualsTo(PObject value)
        {
            if (value is PNumber)
            {
                return this.Value == (value as PNumber).Value ? PBool.True : PBool.False;
            }
            throw new Exception("Could not mod value " + value + " to a number");
        }

        public override PBool NotEqualsTo(PObject value)
        {
            if (value is PNumber)
            {
                return this.Value != (value as PNumber).Value ? PBool.True : PBool.False;
            }
            throw new Exception("Could not mod value " + value + " to a number");
        }

        public override string ToString()
        {
            return this.Value.ToString();
        }

        public override PObject Clone()
        {
            return new PNumber(this.Value);
        }
    }

    public class PList : PObject
    {
        public List<PObject> Value { get; set; }
        public PList(List<PObject> list)
        {
            this.Value = list;
            this.InitializeBuiltinFunctions();
        }

        private void InitializeBuiltinFunctions()
        {
            this.Members.DefineAtCurrentScope("append", new PBuiltinFunction((parameters, environment) =>
            {
                if (parameters.Count != 2)
                {
                    throw new Exception("invalid parmaeters count");
                }
                var value = parameters[1];
                if (value is PNumber || value is PString || value is PBool)
                {
                    value = value.Clone();
                }
                this.Value.Add(value);
                return null;
            }));

            this.Members.DefineAtCurrentScope("pop", new PBuiltinFunction((parameters, environment) =>
            {
                if (parameters.Count != 1)
                {
                    throw new Exception("invalid parmaeters count");
                }
                if (this.Value.Count == 0)
                {
                    throw new Exception("list is empty");
                }
                PObject result = this.Value[this.Value.Count - 1];
                this.Value.RemoveAt(this.Value.Count - 1);
                return result;
            }));
        }

        public override PObject DotGet(PObject value)
        {
            if (value is PString)
            {
                string name = (value as PString).Value;
            }
            return base.DotGet(value);
        }

        public override string ToString()
        {
            return "[" + string.Join(", ", this.Value.Select(s => s.ToString())) + "]";
        }

        public override PObject Add(PObject value)
        {
            if (value is PList)
            {
                PList list = value as PList;
                var objects = new List<PObject>();
                objects.AddRange(this.Value);
                objects.AddRange(list.Value);
                return new PList(objects);
            }
            throw new Exception("list must add to a list");
        }

        public override PObject Mul(PObject value)
        {
            if (value is PNumber)
            {
                PNumber times = value as PNumber;
                var objects = new List<PObject>();
                for (int i = 0; i < times.Value; i++)
                {
                    objects.AddRange(this.Value.Select(p=>p.Clone()));
                }
                return new PList(objects);
            }
            throw new Exception("list must mul to a number");
        }

        public override PObject IndexGet(PObject index)
        {
            if (index is PNumber)
            {
                int actualIndex = (index as PNumber).Value;
                if (actualIndex < 0)
                {
                    actualIndex += this.Value.Count;
                }
                if (actualIndex < 0 || actualIndex >= this.Value.Count)
                {
                    throw new Exception("list index out of range");
                }
                return this.Value[actualIndex];
            }
            else
            {
                throw new Exception("list index must be number");
            }
        }

        public override PObject IndexSet(PObject index, PObject newValue)
        {
            if (index is PNumber)
            {
                int actualIndex = (index as PNumber).Value;
                if (actualIndex < 0)
                {
                    actualIndex += this.Value.Count;
                }
                if (actualIndex < 0 || actualIndex >= this.Value.Count)
                {
                    throw new Exception("list index out of range");
                }
                this.Value[actualIndex] = newValue;
                return this.Value[actualIndex];
            }
            else
            {
                throw new Exception("list index must be number");
            }
        }

        public override PObject Clone()
        {
            return new PList(this.Value.ToList());
        }
    }

    public class PBuiltinFunction : PObject
    {
        private Func<List<PObject>, PEnvironment, PObject> function;
        public PBuiltinFunction(Func<List<PObject>, PEnvironment, PObject> actualFunc)
        {
            this.function = actualFunc;
        }

        public override PObject Invoke(List<PObject> arguments, PEnvironment environment)
        {
            return this.function(arguments, environment);
        }

        public override string ToString()
        {
            return "builtin function";
        }
    }

    public class PFunction : PObject
    {
        public List<string> Parameters { get; private set; }
        public PNode Body { get; private set; }

        public PFunction(List<string> parameters, PNode node)
        {
            this.Parameters = parameters;
            this.Body = node;
        }

        public override PObject Clone()
        {
            return this;
        }

        public override PObject Invoke(List<PObject> arguments, PEnvironment environment)
        {
            try
            {
                environment = new PEnvironment(environment);
                int min = Math.Min(Parameters.Count, arguments.Count);
                for (int i = 0; i < min; i++)
                {
                    var argument = arguments[i];
                    if (argument is PNumber || argument is PString)
                    {
                        argument = argument.Clone();
                    }
                    environment.Define(this.Parameters[i], argument);
                }

                if (this.Body is PNodeArray)
                {
                    foreach (var item in (this.Body as PNodeArray).ArrayObject)
                    {
                        item.Walk(environment);
                    }
                    return PObject.Null;
                }
                else
                {
                    PObject result = this.Body.Walk(environment);
                    return result;
                }
            }
            catch (PReturnException ex)
            {
                return ex.Result;
            }
            finally
            {
                environment = environment.Parent;
            }
        }

        public override string ToString()
        {
            return "function object";
        }
    }
}
